package com.xhj.mybatis.executor;

import com.xhj.mybatis.cache.Cache;
import com.xhj.mybatis.cache.CacheKey;
import com.xhj.mybatis.cache.TransactionalCacheManager;
import com.xhj.mybatis.mapping.BoundSql;
import com.xhj.mybatis.mapping.MappedStatement;
import com.xhj.mybatis.session.ResultHandler;
import com.xhj.mybatis.session.RowBounds;
import com.xhj.mybatis.transaction.Transaction;

import java.sql.SQLException;
import java.util.List;

/**
 * CachingExecutor
 *
 * @author XJks
 * @description CachingExecutor 是 MyBatis 中的一个执行器实现类，负责在执行数据库操作时添加缓存功能。类似装饰器的功能，它通过委托给另一个实际的执行器来完成数据库操作，同时利用 TransactionalCacheManager 管理缓存的事务性操作，确保缓存的一致性和正确性。
 */
public class CachingExecutor implements Executor {

    /**
     * 委托的实际执行器，用于执行数据库操作
     */
    private final Executor delegate;

    /**
     * 事务缓存管理器，用于管理缓存的事务性操作
     */
    private final TransactionalCacheManager tcm = new TransactionalCacheManager();

    /**
     * 构造方法，接收一个实际的执行器，并设置包装器
     *
     * @param delegate 被委托的执行器
     */
    public CachingExecutor(Executor delegate) {
        // 初始化委托执行器
        this.delegate = delegate;
        /**
         * 设置当前对象为委托执行器的包装器
         * TODO:注意这里是将当前对象传递给委托执行器，委托执行器会持有当前对象的引用，在对应执行器的实现类中也需要使用对应 wrapper 来完成相关操作，示例在 {@link SimpleExecutor#doQuery(MappedStatement, Object, RowBounds, ResultHandler, BoundSql)} 方法中。
         */
        delegate.setExecutorWrapper(this);
    }

    /**
     * 获取当前的事务对象，用于管理数据库事务。
     * 直接委托给实际执行器。
     */
    @Override
    public Transaction getTransaction() {
        return delegate.getTransaction();
    }

    /**
     * 关闭执行器，根据参数决定是否回滚事务，并关闭委托执行器
     *
     * @param forceRollback 是否强制回滚事务
     */
    @Override
    public void close(boolean forceRollback) {
        try {
            if (forceRollback) {
                // 回滚缓存事务
                tcm.rollback();
            } else {
                // 提交缓存事务
                tcm.commit();
            }
        } finally {
            // 委托关闭实际执行器
            delegate.close(forceRollback);
        }
    }

    /**
     * 执行更新操作（如INSERT、UPDATE、DELETE），返回受影响的行数。
     * <p>
     * TODO:
     *  1.在执行更新操作前，根据 MappedStatement 判断是否在执行语句前需要刷新缓存。
     *  2.然后委托实际执行器执行更新操作。
     */
    @Override
    public int update(MappedStatement ms, Object parameterObject) throws SQLException {
        // 根据 MappedStatement 判断是否在执行语句前需要刷新缓存
        flushCacheIfRequired(ms);
        // 委托实际执行器执行更新操作
        return delegate.update(ms, parameterObject);
    }

    /**
     * 执行查询操作，返回结果列表
     */
    @Override
    public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
        // 获取绑定的 SQL 对象
        BoundSql boundSql = ms.getBoundSql(parameterObject);
        // 创建缓存 key
        CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
        /**
         * TODO:
         *  1.下方代码是调用主查询方法
         *  2.在插件机制下的分析:
         *      2.1 该方法 {@link #query(MappedStatement, Object, RowBounds, ResultHandler)} 是只能被外部类 {@link com.xhj.mybatis.session.defaults.DefaultSqlSession} 调用的，外部类调用该方法时，使用的是包装后的执行器对象（插件增强后的执行器对象），是代理对象，所以该方法会被拦截器链处理。
         *      2.2 但是该方法内部调用的主查询方法 {@link #query(MappedStatement, Object, RowBounds, ResultHandler, CacheKey, BoundSql)} 是在当前类中直接调用的，并不是通过包装器调用的，所以该方法调用时使用的仍然是当前类的原始对象，并不是代理对象，所以该方法不会被拦截器链处理。
         *      2.3 也就是说，插件只能拦截外部调用的查询方法，无法拦截执行器内部调用的查询方法。
         *  3.因此，如果需要拦截执行器的查询操作，插件只能拦截外部调用的查询方法，无法拦截执行器内部调用的查询方法。
         *  4.这也是 MyBatis 插件机制的一个限制点，用户在使用插件时需要注意这一点。
         *  5.总之，插件机制无法拦截执行器内部调用的查询方法，这是设计上的一个限制。
         */
        return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
    }

    /**
     * 执行查询操作，返回结果列表。
     * TODO:优先从缓存获取结果，否则委托实际执行器并写入缓存。
     */
    @Override
    public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
        // 获取当前 SQL 映射语句关联的二级缓存对象
        Cache cache = ms.getCache();
        // 如果二级缓存对象不为空
        if (cache != null) {
            // 根据 MappedStatement 判断是否在执行语句前需要刷新缓存
            flushCacheIfRequired(ms);
            // 只有开启二级缓存且没有结果处理器时才使用二级缓存
            if (ms.isUseCache() && resultHandler == null) {
                // todo:源码这里存在检查存储过程 OUT 参数的逻辑，目前没有存储过程相关实现，暂不实现。
                /**
                 * 尝试从二级缓存中获取结果
                 * TODO:
                 *  1.该方法 query 方法整体与 {@link BaseExecutor#query(MappedStatement, Object, RowBounds, ResultHandler, CacheKey, BoundSql)} 方法类似。
                 *  2.两者都是先执行 {@link Cache#getObject(Object)} 方法，然后再执行 {@link Cache#putObject(Object, Object)} 方法，根据该逻辑才能衍生出相关装饰器类来实现不同的缓存策略，比如 {@link com.xhj.mybatis.cache.decorators.BlockingCache} 等。
                 *      2.1 先调用 {@link Cache#getObject(Object)} 方法是为了尝试从缓存中获取数据，如果缓存命中则直接返回数据，避免重复查询数据库，则不会调用 {@link Cache#putObject(Object, Object)} 方法。
                 *      2.2 如果缓存未命中，才会调用 {@link Cache#putObject(Object, Object)} 方法将查询结果放入缓存。
                 *  3.不同的是，BaseExecutor 是操作一级缓存，而 CachingExecutor 是操作二级缓存。
                 *  4.在该方法中，实际操作的是 {@link TransactionalCacheManager} 来间接操作这些装饰器类的缓存的，而不是直接操作底层缓存对象。
                 *  5.缓存调用链图示例: CachingExecutor -> TransactionalCacheManager -> TransactionalCache -> Cache Decorators (如 BlockingCache) -> 实际 Cache 实现 (如 PerpetualCache)
                 *      5.1 {@link CachingExecutor}
                 *      5.2 {@link TransactionalCacheManager}
                 *      5.3 {@link com.xhj.mybatis.cache.decorators.TransactionalCache}
                 *      5.4 {@link com.xhj.mybatis.cache.decorators.BlockingCache} 等其他非 TransactionalCache 装饰器类
                 *      5.5 {@link com.xhj.mybatis.cache.impl.PerpetualCache} 等实际缓存实现类
                 */
                List<E> list = (List<E>) tcm.getObject(cache, key);
                // 如果结果为空，说明缓存未命中
                if (list == null) {
                    // 缓存未命中，委托实际执行器查询并
                    list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
                    // 将查询结果放入二级缓存
                    tcm.putObject(cache, key, list);
                }
                // 返回查询结果
                return list;
            }
        }
        // 未使用二级缓存，直接委托实际执行器
        return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
    }

    /**
     * 提交事务，如果required为true则必须提交。
     * TODO:先委托实际执行器，再提交缓存事务.
     */
    @Override
    public void commit(boolean required) throws SQLException {
        // TODO:先委托实际执行器提交事务，该事务具体指的是数据库事务。
        delegate.commit(required);
        /**
         * TODO:
         *  1.再提交缓存事务，该事务具体指的是某一会话 SqlSession 对二级缓存所做的变更，需要将这些变更提交到二级缓存中，供其他会话使用。
         *  2.因为二级缓存是跨会话共享的，所以需要在事务提交时将变更同步到二级缓存，使其他会话能够看到最新的数据。
         *  3.综上所述，下方代码提交的缓存事务，实际上是将当前会话对二级缓存所做的变更提交到二级缓存中。
         */
        tcm.commit();
    }

    /**
     * 回滚事务，如果required为true则必须回滚。
     * TODO:先委托实际执行器，再回滚缓存事务（仅在需要时）.
     */
    @Override
    public void rollback(boolean required) throws SQLException {
        try {
            // 委托实际执行器回滚事务
            delegate.rollback(required);
        } finally {
            if (required) {
                // 回滚缓存事务
                tcm.rollback();
            }
        }
    }

    /**
     * 创建一个缓存键，用于唯一标识某个查询结果，便于缓存管理。
     * 直接委托给实际执行器。
     */
    @Override
    public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
        return delegate.createCacheKey(ms, parameterObject, rowBounds, boundSql);
    }

    /**
     * 判断某个查询是否已经被缓存。
     * 直接委托给实际执行器。
     */
    @Override
    public boolean isCached(MappedStatement ms, CacheKey key) {
        return delegate.isCached(ms, key);
    }

    /**
     * 清除本地缓存，通常在执行更新操作后调用，防止脏读。
     * 直接委托给实际执行器。
     */
    @Override
    public void clearLocalCache() {
        delegate.clearLocalCache();
    }

    /**
     * 根据 MappedStatement 判断是否在执行语句前需要刷新缓存。
     *
     * @param ms MappedStatement 对象
     */
    private void flushCacheIfRequired(MappedStatement ms) {
        // 获取当前 SQL 映射语句关联的缓存对象
        Cache cache = ms.getCache();
        // 如果缓存对象不为空，并且配置了在执行语句前需要刷新缓存
        if (cache != null && ms.isFlushCacheRequired()) {
            // 刷新缓存，TODO:注意这里实际上是调用 TransactionalCacheManager 来清理缓存的
            tcm.clear(cache);
        }
    }

    /**
     * 设置执行器包装器，此方法不应被调用，直接抛异常。
     */
    @Override
    public void setExecutorWrapper(Executor executor) {
        throw new UnsupportedOperationException("This method should not be called");
    }

}
